Pg graphql authenticated table and other updates#160
Merged
Conversation
2132601 to
0f5042e
Compare
deepthi
approved these changes
Apr 26, 2026
dnywh
approved these changes
Apr 27, 2026
Contributor
dnywh
left a comment
There was a problem hiding this comment.
Reviewed from a design perspective only. Naming, pairing, and remediation flow makes sense. Will test on Studio separately.
Signed-off-by: Bobbie Soedirgo <bobbie@soedirgo.dev>
soedirgo
approved these changes
Apr 27, 2026
samrose
added a commit
that referenced
this pull request
Apr 27, 2026
#160 added a pgrst.db_schemas filter to the function lints to match PostgREST's API exposure scope, but the test SQL didn't set the guc, so the positive cases stopped firing. Mirrors the pattern from test/sql/0023_sensitive_columns_exposed.sql.
joshenlim
pushed a commit
to supabase/supabase
that referenced
this pull request
Apr 27, 2026
…R functions (#45260) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature — wires up three new advisor lints landed in splinter, and updates the self-hosted SQL bundle for the existing `pg_graphql_anon_table_exposed` lint to track splinter's correctness fixes. Companion to `supabase/splinter` #160 (already merged) and #162 (test fix in flight). ## What is the current behavior? Splinter's `main` now exposes four lints in the pg_graphql / SECURITY DEFINER family: - `pg_graphql_anon_table_exposed` (0026, existing) — wired into Studio in #45253; SQL in `packages/pg-meta` is the original version that uses `has_table_privilege` and the relkind set `('r','p','v','m')`. - `pg_graphql_authenticated_table_exposed` (0027, new) — paired check against the `authenticated` role. Studio renders any new finding without a `lintInfoMap` entry as a row with no icon, no title mapping, and no "Fix" CTA. Self-hosted users do not see the lint at all because `packages/pg-meta` does not include it. - `anon_security_definer_function_executable` (0028, new) — `SECURITY DEFINER` function executable by `anon`. Same Studio + self-hosted gaps as 0027. - `authenticated_security_definer_function_executable` (0029, new) — same against `authenticated`. Splinter has also updated 0026 itself (PR #160) in two ways that need to flow into the self-hosted SQL bundle: 1. **`relkind` filter:** `('r','p','v','m')` → `('r','v','m','f')`. Drops partitioned table roots (pg_graphql does not expose them; their leaf partitions are still covered as `'r'`) and adds foreign tables, which pg_graphql does expose. 2. **Privilege predicate:** `has_table_privilege(role, oid, 'SELECT')` → `EXISTS` over `pg_attribute` calling `has_column_privilege`. Catches column-level grants such as `GRANT SELECT (col) ON t TO anon`, which pg_graphql's introspection exposes but `has_table_privilege` missed. Cloud projects auto-fetch `splinter.sql` via the platform mgmt-api's `getLintSql` (1-hour cache TTL), so they pick up #160's lint and SQL changes independently of this PR. This PR is about the Studio display mapping and the self-hosted SQL bundle. ## What is the new behavior? Two minimal additions, mirroring the integration shape of #45253. ### `apps/studio/components/interfaces/Linter/Linter.utils.tsx` Three new entries appended to `lintInfoMap`: - `pg_graphql_authenticated_table_exposed` — `Eye` icon (paired with the existing `pg_graphql_anon_table_exposed` entry); link points to the Table Editor scoped to `metadata.schema` + `metadata.name`; `linkText: 'View object'`; `category: 'security'`. - `anon_security_definer_function_executable` — `Unlock` icon (signals "this thing is callable when it shouldn't be"); link points to the Database Functions browser scoped to `metadata.schema` + `metadata.name`; `linkText: 'View function'`; `category: 'security'`. - `authenticated_security_definer_function_executable` — same as 0028 against `authenticated`. Each entry's `docsLink` points at the splinter-hosted lint doc. ### `packages/pg-meta/src/sql/studio/advisor/lints.ts` The existing `pg_graphql_anon_table_exposed` SQL block is updated in place to match the new splinter version: new `relkind` set, `case` statement for `'f'`, and the `EXISTS` over `pg_attribute` privilege check. Three new `union all` blocks are appended for 0027/0028/0029. The function lints (0028/0029) include the `pgrst.db_schemas` filter (mirroring lint `0023_sensitive_columns_exposed`) so findings are scoped to schemas PostgREST actually exposes; the self-hosted query wrapper already sets the GUC when `exposedSchemas` is passed (`enrichLintsQuery`). ## Coverage of the four exposure paths | Role | Tables/views/MVs/foreign tables | SECURITY DEFINER functions | |------|---------|----------| | `anon` | 0026 (existing, updated) | 0028 (new) | | `authenticated` | 0027 (new) | 0029 (new) | The 0026/0027 pair covers `pg_graphql` introspection visibility; the 0028/0029 pair covers RLS bypass via privileged function execution through `/rest/v1/rpc` (and `/graphql/v1` for compatible return types). Each lint's doc cross-references its sibling so an operator hitting one is steered toward the others. ## Verification - `cd packages/pg-meta && npx tsc --noEmit` — clean. - `cd apps/studio && npx tsc --noEmit` — clean for the changed file. (Other unrelated TS errors exist in the working tree but are pre-existing and not introduced by this PR.) - `cd apps/studio && npx eslint components/interfaces/Linter/Linter.utils.tsx` — clean. ## Files - `apps/studio/components/interfaces/Linter/Linter.utils.tsx` — adds three `lintInfoMap` entries (0027, 0028, 0029). - `packages/pg-meta/src/sql/studio/advisor/lints.ts` — updates the 0026 SQL block to match splinter's correctness fixes, appends 0027/0028/0029 SQL blocks. ## Related - supabase/splinter#160 — adds 0027/0028/0029 and rewrites 0026 (merged). - supabase/splinter#162 — fixes test setup for 0028/0029 (in flight; does not affect the SQL shipped here). - #45253 — original 0026 Studio integration. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added security linting to detect authenticated-table exposure and executable SECURITY DEFINER functions. * Added signed-in visibility checks alongside anonymous checks. * **Bug Fixes / Improvements** * Improved relation type handling for accurate table/foreign/partition classification. * Switched to column-level privilege analysis for visibility. * Improved entity naming shown in lints (includes function argument display). <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes correctness gaps in splinter's pg_graphql introspection coverage and adds a paired pair of lints for the SECURITY DEFINER function exposure path, which is often the largest data-leak surface in practice. After this PR, the four lints below cover the four direct exposure paths to public-API roles: anon-vs-authenticated ×
tables-vs-functions.
The four lints are:
All four are EXTERNAL, SECURITY. The name column on 0026 is unchanged so existing Studio integrations (supabase/supabase #45253) keep matching.
Conditions covered
0026 — pg_graphql introspection (anon)
Fires when all of the following are true: the pg_graphql extension is installed; the relation lives in a non-system schema; the relation's relkind is one of 'r' (table), 'v' (view), 'm' (materialized view), or 'f' (foreign table); and the anon role has SELECT on at least one column of the relation. The privilege check is
column-level (has_column_privilege over pg_attribute), so both table-level grants and column-only grants such as GRANT SELECT (col) ON t TO anon fire the lint. Metadata exposed: schema, name, type.
0027 — pg_graphql introspection (authenticated)
Same conditions as 0026 against the authenticated role. This is the second half of the introspection check: in default Supabase projects anon and authenticated start with identical default-privilege grants, so revoking from one alone usually leaves the introspection response unchanged for the other. An operator following the
original 0026 doc verbatim — "revoke from anon, grant to authenticated" — would clear 0026 and still serve a byte-for-byte unchanged introspection response to every signed-up user. 0027 catches that residual exposure. Metadata exposed: schema, name, type.
0028 — SECURITY DEFINER function executable by anon
Fires when all of the following are true: the function lives in a non-system schema; the function has prosecdef = true; and the anon role has EXECUTE on the function (direct grant, role membership, or via PUBLIC — which is the Postgres default for new functions). Does not gate on pg_graphql being installed: PostgREST exposes the
function at /rest/v1/rpc/ independently. Metadata exposed: schema, name, arguments, language, security_definer. The arguments value comes from pg_get_function_identity_arguments so overloaded functions produce one finding per signature with a distinct cache_key.
SECURITY INVOKER functions are deliberately excluded — they execute as the caller and RLS still applies to any tables they touch. The risk pattern these lints target is RLS bypass via privilege escalation in SECURITY DEFINER. The underlying-table risk for SECURITY INVOKER functions is owned by lints 0008 and 0013.
0029 — SECURITY DEFINER function executable by authenticated
Same conditions as 0028 against the authenticated role. Same metadata shape.
Changes to existing 0026 behaviour
0026 was originally added in PR #158. Three correctness gaps are fixed in this PR.
The first is role coverage. The original lint only checked anon. pg_graphql introspection runs under whichever role the caller's JWT claims, so anon and authenticated see independent introspection responses. The original remediation in the docs ("revoke from anon, grant to authenticated") could be followed verbatim and leave the
introspection response served to every signed-up user byte-for-byte unchanged. The fix is to keep 0026 focused on anon, add a parallel 0027 for authenticated, and rewrite the docs to make the pairing explicit.
The second is the relkind filter. The original filter was ('r','p','v','m'). pg_graphql's actual filter at load_sql_context.sql:395-400 is ('r','v','m','f'). The fix is to drop 'p' (partitioned table roots are not exposed by pg_graphql; their leaf partitions remain covered as 'r') and add 'f' (foreign tables, which pg_graphql
does expose).
The third is privilege-predicate granularity. The original used has_table_privilege(role, oid, 'SELECT'). pg_graphql actually uses has_column_privilege per column at load_sql_context.sql:327 (populating is_selectable) and sql_types.rs:594 (is_any_column_selectable returning columns.iter().any(|x| x.permissions.is_selectable)). A
relation where the role has only a column-level grant — GRANT SELECT (some_col) ON t TO anon — would be exposed by pg_graphql but missed by the lint. Insert/Update/Delete entrypoints share the same prerequisite (graphql.rs:194-209), so this gap also masked mutation exposures. The fix is to replace has_table_privilege with an
EXISTS over pg_attribute calling has_column_privilege, filtered to live columns (attnum > 0 AND NOT attisdropped). This is a strict superset of the previous behaviour: anything that fired before still fires, and column-only grants now fire too.
Why these are paired in one PR
The four lints are mutually reinforcing. An operator who acts on 0026 alone could clear it and still be wide open via 0027, 0028, or 0029. Shipping them together so the docs can cross-link, the remediation guidance can land coherently, and Studio can render the related findings as a coherent group avoids review thrash and
prevents a partial mitigation from looking complete. The pg_graphql introspection lints (0026/0027) and the SECURITY DEFINER function lints (0028/0029) cover related-but-distinct exposure paths; the docs cross-reference each other so an operator reading any one finding is pointed at the others.
Documentation
Each doc has the same up-front structure: level, summary, ramification, a "see also" call-out cross-linking the paired lint, and an "If you are not using pg_graphql, disable it" call-out near the top. For 0026/0027 disabling pg_graphql fully closes the introspection surface. For 0028/0029 it only closes the /graphql/v1 half —
the function is still callable via PostgREST /rest/v1/rpc, so the lint keeps firing and the rest of the doc still applies. The doc says so explicitly in each case.
The remediation sections cover the same three options across all four lints: revoke (always including PUBLIC because Postgres default grants live there), narrower per-relation revoke for the targeted finding, and full endpoint shutdown for projects that do not use /graphql/v1 at all. Each doc also has verification snippets
showing the exact curl calls to confirm the fix from both /graphql/v1 and /rest/v1/rpc, plus a quick-reference SQL table and a false-positives section.
Test coverage
0026's test covers: baseline (no extension, 0 rows); pg_graphql-not-installed negative; positive on table + view + materialized view together (all three relkinds in the filter, asserting 3 rows alphabetically ordered); revoke-clears resolution; a foreign-table positive case proving relkind='f' fires; a partitioned-table mixed
case proving the root relkind='p' does NOT fire while the leaf 'r' does; a negative case where anon is revoked but authenticated keeps SELECT (the exact "operator followed the original 0026 doc" state, asserts 0 rows); and a column-only-grant positive case proving GRANT SELECT (id) ON t TO anon fires the lint without any
table-level or PUBLIC grant.
0027's test mirrors the same shape against authenticated.
0028's test covers: baseline; SECURITY INVOKER negative (proving the prosecdef = true filter works); SECURITY DEFINER positive with the default PUBLIC EXECUTE grant (the most common accidental exposure, since Postgres' default is EXECUTE to PUBLIC); revoke-clears resolution; an overload positive case proving two functions
sharing a name with different argument lists produce two findings with distinct cache_keys driven by pg_get_function_identity_arguments; a DEFINER-but-EXECUTE-revoked negative; and a DEFINER-in-system-schema negative confirming the exclusion list.
0029's test mirrors the same shape against authenticated.
queries_are_unionable is extended with the two new union members and confirms column-shape parity across the suite.
All 28 tests pass under bin/installcheck against supabase/postgres:15.1.1.13.
Other changes
bin/installcheck adds -f lints/0027*.sql -f lints/0028*.sql -f lints/0029*.sql so the new lints load alongside the rest. splinter.sql is regenerated via bin/compile.py (1547 → 1810 lines) and includes all four lints in the bundled UNION ALL.
The shared system-schema exclusion list — copied across most lint files — listed 'pgtle' twice. This was swept across 18 lint files. Purely cosmetic, no behavioural change, but worth doing once to prevent future copy-paste.
Verification
bin/compile.py
SUPABASE_VERSION=15.1.1.13 docker-compose -f dockerfiles/docker-compose.yml run --build --rm test
All 28 tests passed.
Files
New: lints/0027_pg_graphql_authenticated_table_exposed.sql, lints/0028_anon_security_definer_function_executable.sql, lints/0029_authenticated_security_definer_function_executable.sql, the matching three docs under docs/, the matching three test SQL files under test/sql/, and the matching three expected-output files under
test/expected/.
Modified: lints/0026_pg_graphql_anon_table_exposed.sql for relkind / column-level privilege / comments / cross-reference; docs/0026_pg_graphql_anon_table_exposed.md for the disable-extension callout / relkind note / cross-reference / remediation alignment; test/sql/0026_pg_graphql_anon_table_exposed.sql and its expected output;
test/sql/queries_are_unionable.sql and its expected output; bin/installcheck; splinter.sql. Plus 17 other lints/*.sql files where the duplicate 'pgtle' was swept from the system-schema exclusion list.